package cz.mff.cuni.dpp.liboptions;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Výsledek parsování vstupu. Instanci získáte voláním
 * {@link Options#process(String[])}.
 * 
 * <p>
 * Pomocí {@link #isPresent(Option)} otestujete, zda-li byl {@link Option}
 * použit, pomocí {@link #getValue(Option)} můžete získat jeho hodnotu. (ta je
 * uložena pouze pokud {@link #isValid(Option)} pro daný {@link Option} vrací
 * true.
 * </p>
 * <p>
 * To jestli byl vstup validní zjistíte pomocí {@link #hasErrors()}, pokud vrací
 * true, můžete pomocí {@link #getErrors()} zíksat přístup k vlastním problémům
 * ({@link ParsingErrors}).
 * </p>
 * <p>
 * Přístup k obyčejným argumentům získate pomocí {@link #getArguments()}.
 * </p>
 * 
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class Result {

	private static final Logger logger = LoggerFactory.getLogger(Result.class);

	private final Set<Option<?>> allOptions = new HashSet<Option<?>>();
	private final Set<Option<?>> presentOptions = new HashSet<Option<?>>();
	private final Set<Option<?>> validOptions = new HashSet<Option<?>>();
	private final List<String> arguments = new LinkedList<String>();

	private final Map<Option, Object> values = new HashMap<Option, Object>();

	private final ParsingErrors errors = new ParsingErrors();

	public Result(Set<Option<?>> allOptions) {
		this.allOptions.addAll(allOptions);
	}

	/**
	 * Zavolá {@link OptionCallback}, který bzl nastaven pomocí
	 * {@link Option.Builder#setCallback(OptionCallback)}. Pokud takovy nasatven
	 * nebyl nic se nestane.
	 * <p>
	 * Pro {@link Option} kde {@link #isPresent(Option)} je <code>true</code> se
	 * volá {@link OptionCallback#handleOptionValue(Object)}, pokud je parametr
	 * isMissing nastaven na <code>true</code> tak se pro ostatni parametry vola
	 * {@link OptionCallback#handleNotPresent()}.
	 * 
	 * 
	 * @param callMissing
	 *            pokud je true zavola callback i na nepritomnych parametrech
	 */
	public void callCallbacks(boolean callMissing) {
		Collection<Option<?>> optionsToCall;
		if (callMissing) {
			optionsToCall = allOptions;
		} else {
			optionsToCall = presentOptions;
		}

		for (Option option : optionsToCall) {
			if (presentOptions.contains(option)) {
				if (option.getCallback() != null) {
					option.getCallback().handleOptionValue(values.get(option));
				}
			} else {
				if (option.getCallback() != null) {
					option.getCallback().handleNotPresent();
				}
			}
		}

	}

	/**
	 * @param option
	 *            stejná isntance jako bzla přidána pomocí
	 *            {@link Options#addOptionDefinition(Option)}.
	 * @return <code>true</code> pokud byl option na vstupu
	 * 
	 * @see #isPresent(Option)
	 * @see #getValue(Option)
	 */
	public boolean isPresent(Option<?> option) {
		return presentOptions.contains(option);
	}

	/**
	 * @param option
	 *            stejná isntance jako bzla přidána pomocí
	 *            {@link Options#addOptionDefinition(Option)}.
	 * @return <code>true</code> pávě tehdy, když byl option přítomen (
	 *         {@link #isPresent(Option)}) a zároveň má validní hodnotu. (má
	 *         správný typ a byla v rozsahu opřípadě výčtu)
	 * 
	 * @see #isPresent(Option)
	 * @see #getValue(Option)
	 */
	public boolean isValid(Option<?> option) {
		return validOptions.contains(option);
	}

	/**
	 * 
	 * @param option
	 *            stejná isntance jako bzla přidána pomocí
	 *            {@link Options#addOptionDefinition(Option)}.
	 * @return hodnota a parametru uvedená na vstupu ({@link Boolean#TRUE} pro
	 *         {@link SwitchOption}), nebo <code>null</code> pokud option nebyl
	 *         použit, nebo měl nevalidní hodnotu.
	 * 
	 * @see #isPresent(Option)
	 * @see #getValue(Option)
	 */
	public <T> T getValue(Option<T> option) {
		return (T) values.get(option);
	}

	/**
	 * @return množinu všech přítomných Option (editace množiny nemá side
	 *         effekty)
	 */
	public Set<Option> getPresentOptions() {
		return new HashSet<Option>(presentOptions);
	}

	/**
	 * @return množinu Option, které byli v definici, ale nebzli obsaženy na
	 *         vstupu (editace množiny nemá side effekty)
	 */
	public Set<Option> getMissingOptions() {
		Set<Option> toReturn = new HashSet<Option>(allOptions);
		toReturn.removeAll(presentOptions);
		return toReturn;
	}

	/**
	 * @return množinu všechn Option pro ketré, {@link #isValid(Option)} vrací
	 *         <code>true</code> (editace množiny nemá side effekty)
	 */
	public Set<Option> getValidOptions() {
		return new HashSet<Option>(validOptions);
	}

	/**
	 * @return seznam všech <em>Obzčejných argumentů</em>
	 */
	public List<String> getArguments() {
		logger.debug("Getting all arguments: " + arguments.size());
		return new LinkedList<String>(arguments);
	}

	/**
	 * @return chyby nalezené při parsování.
	 */
	public ParsingErrors getErrors() {
		return errors;
	}

	/**
	 * Zkratka za <code>getErrors().hasErrors()</code>.
	 * 
	 * 
	 * @return {@link ParsingErrors#hasErrors()} pro {@link #getErrors()}
	 */
	public boolean hasErrors() {
		return errors.hasErrors();
	}

	void addArgument(String argument) {
		logger.debug("Adding argument: " + argument);
		arguments.add(argument);
	}

	/**
	 * Prida i do {@link #values} s hodnotou <code>null</code>
	 * 
	 * @param option
	 */
	void addPresentOption(Option<?> option) {
		if (presentOptions.contains(option)) {
			errors.addMultipleUsage(option);	
		}
		
		presentOptions.add(option);

		// nenapadlo me jak to udelat elengantneji
		if (option instanceof SwitchOption) {
			putValue(option, Boolean.TRUE);
		} else {
			putValue(option, null);
		}
	}

	void putValue(Option<?> option, Object value) {
		values.put(option, value);
		if (value != null) {
			validOptions.add(option);
		}
	}

	/**
	 * Zkontroluje, ktere povinne chybi a u kterych chybi hodnota
	 */
	void validate() {
		for (Option<?> option : allOptions) {
			if (option.isObligatory() && !presentOptions.contains(option)) {
				errors.addMissingObligatory(option);
			}
		}

		for (Entry<Option, Object> valueEntry : values.entrySet()) {
			if (valueEntry.getValue() == null) {
				errors.addMissingValue(valueEntry.getKey());
			}
		}

	}

}
